package com.macro.mall.common.lock.redis;


import com.macro.mall.common.lock.LockUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.nio.ByteBuffer;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author 11519
 * @date 2023年 08月17日 15:36:21
 */
@Slf4j
public class RedisWriteLock {
    static byte[] lockScript = null;
    static byte[] releaseScript = null;
    /**
     * 默认获取锁的等待时间
     */
    private final Long DEFAULT_WAIT_TIME = 2000L;

    static {
        String lockScriptBuilder = "if redis.call('SET',KEYS[1],ARGV[1],'NX','PX',ARGV[2]) then " +
                "redis.call('SET',KEYS[2],1,'PX',ARGV[2]);" +
                "return 1;" +
                "else " +
                "if (redis.call('GET',KEYS[1])== ARGV[1]) then " +
                "local count = redis.call('GET',KEYS[2]);" +
                "if not count then " +
                "redis.call('SET',KEYS[2],1,'PX',ARGV[2]);" +
                "return 1;" +
                "else " +
                "count = tonumber(count) + 1;" +
                "redis.call('SET',KEYS[2],count,'PX',ARGV[2]);" +
                "return count;" +
                "end;" +
                "else " +
                "return 0;" +
                "end;" +
                "end;";
        lockScript = lockScriptBuilder.getBytes();


        String releaseScriptBuilder = "if (redis.call('GET',KEYS[1])== ARGV[1]) then " +
                "local count = redis.call('GET',KEYS[2]);" +
                "if count then " +
                "if (tonumber(count) > 1) then " +
                "count = tonumber(count) - 1;" +
                "local live = redis.call('PTTL',KEYS[2]);" +
                "redis.call('SET',KEYS[2],count,'PX',live);" +
                //success unlock reentrant-write-lock
                "return count;" +
                "else " +
                "redis.call('DEL',KEYS[2]);" +
                "redis.call('DEL',KEYS[1]);" +
                //success unlock
                "return 0;" +
                "end;" +
                "else " +
                "redis.call('DEL',KEYS[1]);" +
                "return 0;" +
                "end;" +
                "else " +
                //fail unlock, thread not get the lock
                "return -1;" +
                "end;";
        releaseScript = releaseScriptBuilder.getBytes();
    }

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public boolean lock(String lockKey){
       return tryLock(lockKey, null, 30, TimeUnit.SECONDS);
    }

    public boolean lock(String lockKey, long expireTime, TimeUnit unit){
        return tryLock(lockKey, null, expireTime, unit);
    }


    /**
     * 获取锁
     * @param lockKey 锁key
     * @param waitTime 等待时间
     * @param expireTime 超时时间
     * @param unit 时间单位
     * @return boolean
     */
    public boolean tryLock(String lockKey, Long waitTime, long expireTime, TimeUnit unit){
        if (waitTime < 0){
            throw new RuntimeException("获取锁的时候，等待时间不能为负数");
        }
        long waitUntilTime =0;
        if(waitTime == null){
            waitUntilTime = unit.toMillis(waitTime) + System.currentTimeMillis();
        }else{
            waitUntilTime = DEFAULT_WAIT_TIME + System.currentTimeMillis();
        }

        Long leastTimeLong = unit.toMillis(expireTime);

        for(;;){
            if(System.currentTimeMillis() > waitUntilTime){
                return false;
            }
            Long res = redisTemplate.execute((RedisCallback<Long>) connection -> (Long) connection.eval(lockScript, ReturnType.INTEGER, 2, LockUtil.getWriteLockKey(lockKey).getBytes(), LockUtil.getReentrantWriteLockKey(lockKey).getBytes(), LockUtil.getThreadUid().getBytes(), longToBytes(leastTimeLong)));

            if(res.equals(1L)){
                //获取锁成功
                break;
            }else if(res.equals(0L)){
                if (log.isDebugEnabled()) {
                    log.debug("wait write lock release,  writeLock = {}", LockUtil.getWriteLockKey(lockKey));
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(50);
                } catch (InterruptedException e) {
                    log.error("wait write lock release exception", e);
                }
            }else{
                if (log.isDebugEnabled()) {
                    log.debug("success in reentrant write lock,  reentrantWriteLock = {}, count now = {}", LockUtil.getReentrantWriteLockKey(lockKey), res);
                }
                break;
            }
        }
        return true;
    }

    /**
     * 释放锁
     * @param lockKey key
     */
    public void unlock(String lockKey){
        Long res = redisTemplate.execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return (Long) connection.eval(releaseScript, ReturnType.INTEGER, 2, LockUtil.getWriteLockKey(lockKey).getBytes(), LockUtil.getReentrantWriteLockKey(lockKey).getBytes(), LockUtil.getThreadUid().getBytes());
            }
        });
        if(Objects.equals(res, 0L)){
            if (log.isDebugEnabled()) {
                log.debug("成功释放写锁,  writeLock = {}", LockUtil.getWriteLockKey(lockKey));
            }
        }else if(Objects.equals(res, -1L)){
            if (log.isDebugEnabled()) {
                log.debug("释放锁失败, 线程不能获取到锁,  writeLock = {}, thread = {}", LockUtil.getReentrantWriteLockKey(lockKey), LockUtil.getThreadUid());
            }
        }else {
            if (log.isDebugEnabled()) {
                log.debug("成功释放重入锁,  reentrantWriteLock = {}, count left = {}", LockUtil.getReentrantWriteLockKey(lockKey), res);
            }
        }
    }

    /**
     * long类型转byte[]
     * @param value 值
     * @return byte[]
     */
    public static byte[] longToBytes(long value){
        //分配缓冲区，单位为字节，一个long类型占8个字节，所以分配为8
        ByteBuffer byteBuffer = ByteBuffer.allocate(Long.SIZE/Byte.SIZE);
        byteBuffer.putLong(0,value);
        return byteBuffer.array();
    }
}
